"""
Extension Loader

Dynamic discovery and loading of GSM extensions from various sources.
"""

import importlib
import importlib.util
import inspect
import pkgutil
import sys
from pathlib import Path
from typing import List, Dict, Optional, Any, Type, Set
import logging
import traceback
from .base_extension import BaseExtension
from .extension_registry import registry

logger = logging.getLogger(__name__)


class ExtensionLoader:
    """
    Handles discovery and loading of GSM extensions.
    
    Supports loading extensions from:
    - plugins/ directory (directory-based)
    - setuptools entry points (package-based)
    - explicit module paths
    """
    
    def __init__(self):
        self._loaded_modules: Set[str] = set()
        self._failed_loads: Dict[str, str] = {}
    
    def discover_and_load_all(self, plugins_dir: Optional[Path] = None,
                             entry_point_group: str = "gsm.extensions") -> int:
        """
        Discover and load all available extensions.
        
        Args:
            plugins_dir: Directory to scan for plugins (defaults to ./plugins)
            entry_point_group: Entry point group name for package extensions
            
        Returns:
            Number of extensions successfully loaded
        """
        loaded_count = 0
        
        # Load from plugins directory
        if plugins_dir is None:
            plugins_dir = Path("plugins")
        
        if plugins_dir.exists():
            loaded_count += self.load_from_directory(plugins_dir)
        
        # Load from entry points
        loaded_count += self.load_from_entry_points(entry_point_group)
        
        logger.info(f"Loaded {loaded_count} extensions from all sources")
        return loaded_count
    
    def load_from_directory(self, plugins_dir: Path) -> int:
        """
        Load extensions from a plugins directory.
        
        Args:
            plugins_dir: Directory containing plugin subdirectories
            
        Returns:
            Number of extensions successfully loaded
        """
        if not plugins_dir.exists():
            logger.warning(f"Plugins directory does not exist: {plugins_dir}")
            return 0
        
        loaded_count = 0
        
        for plugin_path in plugins_dir.iterdir():
            if not plugin_path.is_dir():
                continue
            
            # Skip hidden directories and __pycache__
            if plugin_path.name.startswith('.') or plugin_path.name == '__pycache__':
                continue
            
            try:
                if self._load_plugin_from_directory(plugin_path):
                    loaded_count += 1
            except Exception as e:
                error_msg = f"Failed to load plugin from {plugin_path}: {e}"
                logger.error(error_msg)
                self._failed_loads[plugin_path.name] = error_msg
        
        logger.info(f"Loaded {loaded_count} extensions from directory {plugins_dir}")
        return loaded_count
    
    def load_from_entry_points(self, group_name: str = "gsm.extensions") -> int:
        """
        Load extensions from setuptools entry points.
        
        Args:
            group_name: Entry point group name
            
        Returns:
            Number of extensions successfully loaded
        """
        loaded_count = 0
        
        try:
            # Use importlib.metadata for Python 3.8+
            try:
                from importlib.metadata import entry_points
            except ImportError:
                # Fallback for older Python versions
                from importlib_metadata import entry_points
            
            # Get entry points for the group
            eps = entry_points()
            if hasattr(eps, 'select'):  # Python 3.10+
                group_eps = eps.select(group=group_name)
            else:  # Python 3.8-3.9
                group_eps = eps.get(group_name, [])
            
            for entry_point in group_eps:
                try:
                    extension_class = entry_point.load()
                    if self._load_extension_class(extension_class, entry_point.name):
                        loaded_count += 1
                except Exception as e:
                    error_msg = f"Failed to load entry point {entry_point.name}: {e}"
                    logger.error(error_msg)
                    self._failed_loads[entry_point.name] = error_msg
        
        except ImportError:
            logger.warning("importlib.metadata not available, skipping entry point loading")
        except Exception as e:
            logger.error(f"Error loading from entry points: {e}")
        
        logger.info(f"Loaded {loaded_count} extensions from entry points")
        return loaded_count
    
    def load_from_module(self, module_path: str, extension_name: Optional[str] = None) -> bool:
        """
        Load extension from a specific module path.
        
        Args:
            module_path: Python module path (e.g., 'mypackage.extensions.solver')
            extension_name: Optional name for the extension
            
        Returns:
            True if extension was loaded successfully
        """
        try:
            module = importlib.import_module(module_path)
            return self._scan_module_for_extensions(module, extension_name)
        except Exception as e:
            error_msg = f"Failed to load module {module_path}: {e}"
            logger.error(error_msg)
            self._failed_loads[module_path] = error_msg
            return False
    
    def load_extension_file(self, file_path: Path, extension_name: Optional[str] = None) -> bool:
        """
        Load extension from a specific Python file.
        
        Args:
            file_path: Path to Python file containing extension
            extension_name: Optional name for the extension
            
        Returns:
            True if extension was loaded successfully
        """
        try:
            # Load module from file
            spec = importlib.util.spec_from_file_location(
                file_path.stem, file_path
            )
            if spec is None or spec.loader is None:
                raise ImportError(f"Cannot load spec from {file_path}")
            
            module = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(module)
            
            return self._scan_module_for_extensions(module, extension_name)
        except Exception as e:
            error_msg = f"Failed to load extension file {file_path}: {e}"
            logger.error(error_msg)
            self._failed_loads[str(file_path)] = error_msg
            return False
    
    def get_load_failures(self) -> Dict[str, str]:
        """
        Get information about failed extension loads.
        
        Returns:
            Dictionary mapping extension names to error messages
        """
        return self._failed_loads.copy()
    
    def clear_failures(self) -> None:
        """Clear the failed loads history."""
        self._failed_loads.clear()
    
    def _load_plugin_from_directory(self, plugin_dir: Path) -> bool:
        """
        Load a plugin from a directory structure.
        
        Args:
            plugin_dir: Directory containing the plugin
            
        Returns:
            True if plugin was loaded successfully
        """
        # Look for common entry point files
        entry_files = [
            plugin_dir / "__init__.py",
            plugin_dir / "extension.py",
            plugin_dir / f"{plugin_dir.name}.py",
        ]
        
        for entry_file in entry_files:
            if entry_file.exists():
                return self.load_extension_file(entry_file, plugin_dir.name)
        
        # If no standard files, try to load as package
        if (plugin_dir / "__init__.py").exists():
            try:
                # Add plugins directory to path temporarily
                plugins_parent = plugin_dir.parent
                if str(plugins_parent) not in sys.path:
                    sys.path.insert(0, str(plugins_parent))
                
                return self.load_from_module(plugin_dir.name, plugin_dir.name)
            finally:
                # Clean up path
                if str(plugins_parent) in sys.path:
                    sys.path.remove(str(plugins_parent))
        
        logger.warning(f"No valid entry point found in plugin directory: {plugin_dir}")
        return False
    
    def _scan_module_for_extensions(self, module: Any, preferred_name: Optional[str] = None) -> bool:
        """
        Scan a module for extension classes and register them.
        
        Args:
            module: Module to scan
            preferred_name: Preferred name for single extension
            
        Returns:
            True if at least one extension was found and registered
        """
        extensions_found = []
        
        # Look for extension classes
        for name, obj in inspect.getmembers(module):
            if (inspect.isclass(obj) and 
                issubclass(obj, BaseExtension) and 
                obj != BaseExtension):
                extensions_found.append(obj)
        
        # Look for extension instances
        for name, obj in inspect.getmembers(module):
            if isinstance(obj, BaseExtension):
                extensions_found.append(obj)
        
        # Register found extensions
        registered_count = 0
        for ext in extensions_found:
            try:
                if inspect.isclass(ext):
                    # Instantiate class
                    if preferred_name:
                        extension = ext(preferred_name)
                    else:
                        extension = ext()
                else:
                    # Use instance directly
                    extension = ext
                
                if self._load_extension_instance(extension):
                    registered_count += 1
            except Exception as e:
                logger.error(f"Failed to instantiate extension {ext}: {e}")
        
        return registered_count > 0
    
    def _load_extension_class(self, extension_class: Type[BaseExtension], 
                             name: Optional[str] = None) -> bool:
        """
        Load an extension from a class.
        
        Args:
            extension_class: Extension class to instantiate
            name: Optional name for the extension
            
        Returns:
            True if extension was loaded successfully
        """
        try:
            if name:
                extension = extension_class(name)
            else:
                extension = extension_class()
            
            return self._load_extension_instance(extension)
        except Exception as e:
            logger.error(f"Failed to instantiate extension class {extension_class}: {e}")
            return False
    
    def _load_extension_instance(self, extension: BaseExtension) -> bool:
        """
        Register an extension instance with the registry.
        
        Args:
            extension: Extension instance to register
            
        Returns:
            True if extension was registered successfully
        """
        try:
            # Validate extension before registration
            validation_errors = self._validate_extension_basic(extension)
            if validation_errors:
                logger.error(f"Extension validation failed: {validation_errors}")
                return False
            
            # Register with registry
            success = registry.register(extension)
            if success:
                logger.info(f"Successfully loaded extension: {extension.name}")
            else:
                logger.error(f"Failed to register extension: {extension.name}")
            
            return success
        except Exception as e:
            logger.error(f"Error loading extension {extension}: {e}")
            logger.debug(traceback.format_exc())
            return False
    
    def _validate_extension_basic(self, extension: BaseExtension) -> List[str]:
        """
        Perform basic validation on an extension before registration.
        
        Args:
            extension: Extension to validate
            
        Returns:
            List of validation error messages
        """
        errors = []
        
        try:
            # Check that required methods are implemented and callable
            if not hasattr(extension, 'name') or not extension.name:
                errors.append("Extension must have a non-empty name")
            
            if not hasattr(extension, 'version') or not extension.version:
                errors.append("Extension must have a version")
            
            # Test that abstract methods are implemented
            try:
                extension.description
            except (NotImplementedError, AttributeError):
                errors.append("Extension must implement description property")
            
            try:
                extension.supported_frameworks
            except (NotImplementedError, AttributeError):
                errors.append("Extension must implement supported_frameworks property")
            
            try:
                extension.config_schema
            except (NotImplementedError, AttributeError):
                errors.append("Extension must implement config_schema property")
            
            # Test that required methods exist and are callable
            required_methods = [
                'can_handle_config', 'get_builder', 'get_runner', 'get_validator'
            ]
            
            for method_name in required_methods:
                if not hasattr(extension, method_name):
                    errors.append(f"Extension must implement {method_name} method")
                elif not callable(getattr(extension, method_name)):
                    errors.append(f"Extension {method_name} must be callable")
        
        except Exception as e:
            errors.append(f"Extension validation error: {e}")
        
        return errors
    
    def reload_extension(self, extension_name: str) -> bool:
        """
        Reload an extension (unregister and re-load).
        
        Args:
            extension_name: Name of extension to reload
            
        Returns:
            True if extension was reloaded successfully
        """
        # Unregister existing extension
        if extension_name in registry:
            registry.unregister(extension_name)
        
        # Clear from loaded modules if present
        modules_to_remove = [mod for mod in sys.modules 
                           if extension_name in mod]
        for mod in modules_to_remove:
            del sys.modules[mod]
        
        # Try to reload - this requires knowing where the extension came from
        # For now, just try to discover and load all again
        return self.discover_and_load_all() > 0
    
    def get_loaded_extensions(self) -> List[str]:
        """
        Get list of successfully loaded extension names.
        
        Returns:
            List of extension names
        """
        return list(registry.list_all().keys())
    
    def __len__(self) -> int:
        """Get number of loaded extensions."""
        return len(registry)


# Global loader instance
loader = ExtensionLoader()